package com.guanyou.lock;

import com.guanyou.RedisUtil;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCommands;

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;


public class RedisLock2 {


    // java thread may be slow then using system thread-local
    ThreadLocal<Long> threadId = new ThreadLocal<Long>() {
        @Override
        protected Long initialValue() {
            JedisCommands cmd = redis.getJedis();
            //String keyName = lockName+":"+THREAD_ID;
            //Long id = cmd.incrBy(keyName, 1);
            Long id = cmd.hincrBy(lockName, THREAD_ID, 1);
            redis.returnJedis(cmd);
            return id;
        }
    };


    ThreadLocal<Map<Long, ILockable>> localLocks = new ThreadLocal<Map<Long, ILockable>>() {
        @Override
        protected Map<Long, ILockable> initialValue() {
            return new HashMap<Long, ILockable>();
        }
    };

    public void lock(Long... ids) {
        Arrays.sort(ids, 0, ids.length, new Comparator<Long>() {
            @Override
            public int compare(Long o1, Long o2) {
                long res = o2 - o1;
                return res > 0 ? 1 : (res == 0 ? 0 : -1);
            }
        });
        Map<Long, ILockable> localLocks = this.localLocks.get();
        for (Long id : ids) {
            if (!localLocks.containsKey(id)) {
                ILockable lock = new LockItem(this, id);
                lock.lock();
                localLocks.put(id, lock);
            }
        }
    }

    public void unlockAll() {
        Map<Long, ILockable> localLocks = this.localLocks.get();
        for (ILockable lock : localLocks.values()) {
            lock.unlock();
        }
        localLocks.clear();
    }


    static class LockItem implements ILockable {
        RedisLock2 manager;
        Long res;
        AtomicInteger lockCount = new AtomicInteger(0);
        AtomicLong lockThreadId = new AtomicLong(0);

        LockItem(RedisLock2 manager, long res) {
            this.manager = manager;
            this.res = res;
        }

        @Override
        public void lock() {
            long threadId = manager.threadId.get();
            if (lockCount.get() > 0 && lockThreadId.get() == threadId) {
                lockCount.incrementAndGet();
                return;
            }
            manager.lock(res, threadId);
            lockThreadId.set(threadId);
            lockCount.set(1);
        }

        @Override
        public void unlock() {
            long threadId = manager.threadId.get();
            if (threadId == lockThreadId.get()) {
                if (lockCount.get() > 1) {
                    lockCount.decrementAndGet();
                    return;
                }
                assert lockCount.get() == 1;
                lockCount.set(0);
                lockThreadId.compareAndSet(threadId, 0);
                manager.unlock(res, threadId);
            }
        }

        @Override
        public long getPriority() {
            return res;
        }

        @Override
        public int getLockCount() {
            return lockCount.get();
        }
    }

    public static interface ILockable {
        void lock();

        void unlock();

        long getPriority();

        int getLockCount();
    }

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "EX";
    private static final Long RELEASE_SUCCESS = 1L;
    static final String THREAD_ID = "RedisLock:ThreadID";
    static final int EXPIRE = 30;
    RedisUtil redis;
    String lockName;

    public RedisLock2(RedisUtil redis, String lockName) {
        this.redis = redis;
        this.lockName = lockName;
    }

    protected long getThreadId() {
        JedisCommands cmd = redis.getJedis();
        //String keyName = lockName+":"+THREAD_ID;
        //Long id = cmd.incrBy(keyName, 1);
        Long id = cmd.hincrBy(lockName, THREAD_ID, 1);
        redis.returnJedis(cmd);
        return id;
    }

    protected void lock(Long res, Long threadId) {
        String key = lockName + res;
        String v = "" + threadId;
        JedisCommands cmd = redis.getJedis();
        String value = cmd.get(key);
        if (value != null && v.equals(value)) return;
        while (true) {
            String result = cmd.set(key, v, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 2);
            if (LOCK_SUCCESS.equals(result)) {
                break;
            }
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                //return; // wait up when killed?
            }
        }
        redis.returnJedis(cmd);
    }

    protected void unlock(Long res, Long threadId) {
        Jedis cmd = redis.getRriginalJedis();
        String key = lockName + res;
        String v = "" + threadId;
        assert v.equals(cmd.get(key));
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        cmd.eval(script, Collections.singletonList(key), Collections.singletonList(v));
        redis.returnJedis(cmd);
    }
}
